﻿/// Jasig CAS server emulation with ASP.NET
/// by Uli Trampe
/// http://jasigemu.codeplex.com/
/// 
using System;
using System.Diagnostics;
using System.IO;
using System.Web;
using System.Xml;

namespace Codeplex.JasigEmu
{
    /// <summary>
    /// Static CAS server class that provides the basic Jasig CAS functionality.
    /// </summary>
    public static class CasServer
    {
        /// <summary>
        /// Handle Page_Load from Login page
        /// </summary>
        /// <param name="casAuthenticator"></param>
        /// <returns>Returns true when the call was handled.</returns>
        public static bool HandlePageLoad(ICasAuthenticator casAuthenticator)
        {
            HttpContext httpContext = HttpContext.Current;

            if (IsLogoutRequest(httpContext.Request))
            {
                // logout
                HandleLogoutRequest(httpContext, casAuthenticator);
                return true;
            }
            else if (IsLoginRequest(httpContext.Request) && httpContext.User.Identity.IsAuthenticated)
            {
                // when the user is already authenticated, then directly redirect to the requested service
                HandleLoginRequest(httpContext, casAuthenticator, httpContext.User.Identity.Name, false);
                return true;
            }
            else if (IsServiceValidateRequest(httpContext.Request))
            {
                // ticket service validation CAS 2.0
                string strUserName, strErrorCode, strErrorMsg;
                bool isOK = ValidateTicket(httpContext, casAuthenticator, out strUserName, out strErrorCode, out strErrorMsg);

                string strResponse = BuildServiceValidateResponse(isOK, strUserName, strErrorCode, strErrorMsg);

                httpContext.Response.Clear();
                httpContext.Response.Write(strResponse);
                httpContext.Response.End();
                return true;
            }
            else if (IsValidateRequest(httpContext.Request))
            {
                // ticket validation CAS 1.0
                string strUserName, strErrorCode, strErrorMsg;
                bool isOK = ValidateTicket(httpContext, casAuthenticator, out strUserName, out strErrorCode, out strErrorMsg);

                string strResponse = BuildValidateResponse(isOK, strUserName);

                httpContext.Response.Clear();
                httpContext.Response.Write(strResponse);
                httpContext.Response.End();
                return true;
            }
            
            return false;
        }

        /// <summary>
        /// Handle Login_Click from Login page, after a successfull authentication
        /// </summary>
        /// <param name="casAuthenticator"></param>
        /// <param name="strUserName"></param>
        /// <param name="doRemember"></param>
        /// <returns>Returns true when the call was handled.</returns>
        public static bool HandlePageLogin(ICasAuthenticator casAuthenticator, string strUserName, bool doRemember)
        {
            HttpContext httpContext = HttpContext.Current;

            if (IsLoginRequest(httpContext.Request))
            {
                HandleLoginRequest(httpContext, casAuthenticator, strUserName, doRemember);
                return true;
            }
            
            return false;
        }

        /// <summary>
        /// Handle CAS login request: create a ticket and redirect to the requested service.
        /// </summary>
        /// <param name="httpContext">An System.Web.HttpContext object that contains information associated with the current page.</param>
        /// <param name="casAuthenticator"></param>
        /// <param name="strUserName"></param>
        /// <param name="doRemember"></param>
        private static void HandleLoginRequest(HttpContext httpContext, ICasAuthenticator casAuthenticator, string strUserName, bool doRemember)
        {
            // get the service
            string strService = httpContext.Request.QueryString["service"];
            if (String.IsNullOrEmpty(strService)) return;
            
            // translate the service
            strService = TranslateService(strService, casAuthenticator);

            // call the check permission hook
            if (!casAuthenticator.CasCheckPermission(strUserName, strService)) return;

            // call the login hook
            if (!httpContext.User.Identity.IsAuthenticated) casAuthenticator.CasLogin(strUserName, doRemember);

            // create the CAS ticket based on user name and service
            string strTicket = CasTicket.Issue(httpContext, strUserName, strService);
            
            // build the redirection url
            string strRedirectUrl = strService;
            strRedirectUrl += strService.IndexOf('?') == -1 ? "?ticket=" : "&ticket=";
            strRedirectUrl += strTicket;

            // redirect to the requested service
            httpContext.Response.Redirect(strRedirectUrl);
        }

        /// <summary>
        /// Handle CAS logout request: logout and redirect to the logon page.
        /// </summary>
        /// <param name="httpContext">An System.Web.HttpContext object that contains information associated with the current page.</param>
        /// <param name="casAuthenticator"></param>
        private static void HandleLogoutRequest(HttpContext httpContext, ICasAuthenticator casAuthenticator)
        {
            // call the logout hook
            casAuthenticator.CasLogout();
        }

        /// <summary>
        /// Validate the given ticket and return the corresponding user, or an error code and message
        /// </summary>
        /// <param name="httpContext">An System.Web.HttpContext object that contains information associated with the current page.</param>
        /// <param name="casAuthenticator"></param>
        /// <param name="strUserName"></param>
        /// <param name="strErrorCode"></param>
        /// <param name="strErrorMsg"></param>
        /// <returns>Returns true when the ticket is valid.</returns>
        private static bool ValidateTicket(HttpContext httpContext, ICasAuthenticator casAuthenticator, out string strUserName, out string strErrorCode, out string strErrorMsg)
        {
            strUserName = strErrorCode = strErrorMsg = "";

            // Documentation: http://www.jasig.org/cas/protocol

            string strServiceRequest = httpContext.Request.QueryString["service"];
            if (String.IsNullOrEmpty(strServiceRequest))
            {
                strErrorCode = "INVALID_REQUEST";
                Trace.TraceError("CAS validate request violation - service missing");
                return false;
            }
            string strTicketRequest = httpContext.Request.QueryString["ticket"];
            if (String.IsNullOrEmpty(strTicketRequest))
            {
                strErrorCode = "INVALID_REQUEST";
                Trace.TraceError("CAS validate request violation - ticket missing");
                return false;
            }

            string strService = "";
            strUserName = CasTicket.CheckAndPunch(httpContext, strTicketRequest, ref strService);
            if (strUserName == "")
            {
                strErrorCode = "INVALID_TICKET";
                strErrorMsg = "CAS ticket violation - requested service '" + strServiceRequest + "'";
                Trace.TraceError(strErrorMsg);
                return false;
            }
            strServiceRequest = TranslateService(strServiceRequest, casAuthenticator);
            if (strService != strServiceRequest)
            {
                strErrorCode = "INVALID_SERVICE";
                strErrorMsg = "CAS service violation - requested service '" + strServiceRequest + "' aviable service '" + strService + "' user '" + strUserName + "'";
                Trace.TraceError(strErrorMsg);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Build a response for ticket validation CAS 1.0
        /// </summary>
        /// <param name="isOK">Indicates whether it is a positive response</param>
        /// <param name="strUserName">The user name to be included in the response.</param>
        /// <returns></returns>
        private static string BuildValidateResponse(bool isOK, string strUserName)
        {
            return isOK ? "yes\n" + strUserName + "\n" : "no\n\n";
        }

        /// <summary>
        /// Build a XML response for ticket service validation CAS 2.0
        /// </summary>
        /// <param name="isOK">Indicates whether it is a positive response</param>
        /// <param name="strUserName">The user name to be included in the response.</param>
        /// <param name="strErrorCode">The error code to be included in the response.</param>
        /// <param name="strErrorMsg">The error message to be included in the response.</param>
        /// <returns></returns>
        private static string BuildServiceValidateResponse(bool isOK, string strUserName, string strErrorCode, string strErrorMsg)
        {
            StringWriter writer = new StringWriter();
            XmlTextWriter xmlwriter = new XmlTextWriter(writer);

            xmlwriter.WriteStartElement("cas:serviceResponse");
            xmlwriter.WriteAttributeString("xmlns:cas", "http://www.yale.edu/tp/cas");
            if (isOK)
            {
                xmlwriter.WriteStartElement("cas:authenticationSuccess");
                xmlwriter.WriteStartElement("cas:user");
                xmlwriter.WriteString(strUserName);
                xmlwriter.WriteEndElement();
                xmlwriter.WriteEndElement();
            }
            else
            {
                xmlwriter.WriteStartElement("cas:authenticationFailure");
                xmlwriter.WriteString(strErrorMsg);
                xmlwriter.WriteEndElement();
            }
            xmlwriter.WriteEndElement();

            xmlwriter.Close();
            return writer.ToString();
        }

        /// <summary>
        /// Service translation
        /// </summary>
        /// <param name="strService"></param>
        /// <param name="casAuthenticator"></param>
        /// <returns></returns>
        private static string TranslateService(string strService, ICasAuthenticator casAuthenticator)
        {
            // ignore any jsessionid in the service string, because the Java CAS client tends to request a CAS ticket for a service,
            // and then validates that service including a jsessionid (for whatever reason)
            int nIdx = strService.IndexOf(';');
            if (nIdx != -1) strService = strService.Substring(0, nIdx);

            // call service translation hook
            strService = casAuthenticator.CasTranslateService(strService);

            return strService;
        }

        /// <summary>
        /// Check if the current request is CAS login request.
        /// </summary>
        /// <param name="httpContext">An System.Web.HttpContext object that contains information associated with the current page.</param>
        /// <returns></returns>
        private static bool IsLoginRequest(HttpRequest httpRequest)
        {
            return httpRequest.PathInfo == "/login";
        }

        /// <summary>
        /// Check if the current request is CAS validate (CAS 1.0) request.
        /// </summary>
        /// <param name="httpContext">An System.Web.HttpContext object that contains information associated with the current page.</param>
        /// <returns></returns>
        private static bool IsServiceValidateRequest(HttpRequest httpRequest)
        {
            return httpRequest.PathInfo == "/serviceValidate";
        }

        /// <summary>
        /// Check if the current request is CAS server validate request.
        /// </summary>
        /// <param name="httpContext">An System.Web.HttpContext object that contains information associated with the current page.</param>
        /// <returns></returns>
        private static bool IsValidateRequest(HttpRequest httpRequest)
        {
            return httpRequest.PathInfo == "/validate";
        }

        /// <summary>
        /// Check if the current request is CAS logout request.
        /// </summary>
        /// <param name="httpContext">An System.Web.HttpContext object that contains information associated with the current page.</param>
        /// <returns></returns>
        private static bool IsLogoutRequest(HttpRequest httpRequest)
        {
            if (IsLoginRequest(httpRequest))
            {
                // when a user logs out some Java CAS client applications (like Confluence) do not call the CAS logout request,
                // instead they redirect to the login page, with a parameter indicating that a logout has happened
                string strService = httpRequest.QueryString["service"];
                return !String.IsNullOrEmpty(strService) && strService.Contains("logout=true");
            }
            return httpRequest.PathInfo == "/logout";
        }

    }
}
